package com.zjw.socket.demo.starter.plugin.impl.aop;

import com.zjw.socket.demo.starter.annotation.NonNull;
import com.zjw.socket.demo.starter.annotation.Nullable;
import com.zjw.socket.demo.starter.annotation.core.Plugin;
import com.zjw.socket.demo.starter.core.Reflector;
import com.zjw.socket.demo.starter.core.ioc.IOCBeanInfo;
import com.zjw.socket.demo.starter.core.ioc.IOCObserver;
import com.zjw.socket.demo.starter.core.ioc.IOCScope;
import com.zjw.socket.demo.starter.plugin.IPlugin;
import io.vertx.core.Vertx;
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.Map;


/**
 * 提供AOP能力插件类
 */
@Plugin
public class AspectPlugin implements IPlugin {

  private boolean enableAspect;

  private boolean isInit;

  @Override
  public void onPlugin(Vertx vertx) {
    IOCScope.globalScope.registerRefreshObserver(new IOCObserver() {
      @Override
      public void onRefresh(Class<?> clazz) {

      }

      @Override
      public void onInject(IOCBeanInfo o) {
        // AOP must in inject duration
        parseAOP(o);
      }

      @Override
      public void onComplete() {

      }
    });
  }

  private void parseAOP(IOCBeanInfo beanInfo) {
    if (!isInit) {
      EnableAspect enableAop = Reflector.getApplication().getAnnotation(EnableAspect.class);

      enableAspect = enableAop != null;
      isInit = true;
    }

    if (!enableAspect) {
      return;
    }

    Object object = beanInfo.get();
    Aspect aspect = object.getClass().getAnnotation(Aspect.class);
    Object proxy;
    if (aspect == null) {
      proxy = parseMethodAop(object);
    } else {
      proxy = parseTypeAop(object);
    }
    if (proxy == null) {
      return;
    }

    beanInfo.set(proxy);
  }

  private Object parseMethodAop(Object object) {
    Class<?> objectClass = object.getClass();
    Class<?>[] interfaces = objectClass.getInterfaces();
    boolean checkAspect = false;
    if (interfaces.length > 0) {
      AopInvokeHandler h = new AopInvokeHandler(object);
      Method[] declaredMethods = objectClass.getDeclaredMethods();
      for (Method declaredMethod : declaredMethods) {
        if (declaredMethod.isAnnotationPresent(Aspect.class)) {
          checkAspect = true;
          AopMethodInfo.registerAspectMethod(h.methodMap, object, declaredMethod);
        }
      }
      if (checkAspect) {
        return Proxy.newProxyInstance(objectClass.getClassLoader(), interfaces, h);
      }
    } else {

      //cglib
      CglibMethodInterceptor h = new CglibMethodInterceptor(object);
      Method[] declaredMethods = objectClass.getDeclaredMethods();
      for (Method declaredMethod : declaredMethods) {
        if (declaredMethod.isAnnotationPresent(Aspect.class)) {
          checkAspect = true;
          AopMethodInfo.registerAspectMethod(h.methodMap, object, declaredMethod);
        }
      }

      if (checkAspect) {
        return CglibProxy.newProxy(objectClass, h);
      }
    }
    return null;
  }

  private Object parseTypeAop(Object object) {
    Class<?> objectClass = object.getClass();
    Class<?>[] interfaces = objectClass.getInterfaces();
    if (interfaces.length > 0) {
      //java proxy
      AopInvokeHandler h = new AopInvokeHandler(object);
      AopMethodInfo.registerAopObject(h.methodMap, object);
      return Proxy.newProxyInstance(objectClass.getClassLoader(), interfaces, h);
    } else {
      //cglib
      CglibMethodInterceptor h = new CglibMethodInterceptor(object);
      AopMethodInfo.registerAopObject(h.methodMap, object);
      return CglibProxy.newProxy(objectClass, h);
    }
  }

  ///////////////////////////////////////////////Proxy//////////////////////////////////////////////////////////////////
  static class AopInvokeHandler implements InvocationHandler {

    final Object originObject;

    final Map<Method, AopMethodInfo[]> methodMap = new HashMap<>();

    AopInvokeHandler(Object originObject) {
      this.originObject = originObject;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
      return AopMethodInfo.invoke(methodMap, originObject, proxy, method, null, args);
    }

  }

  static class CglibMethodInterceptor implements MethodInterceptor {
    final Object originObject;
    final Map<Method, AopMethodInfo[]> methodMap = new HashMap<>();

    CglibMethodInterceptor(Object originObject) {
      this.originObject = originObject;
    }

    @Override
    public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
      return AopMethodInfo.invoke(methodMap, originObject, obj, method, proxy, args);
    }
  }

  /////////////////////////////////////////////////////CGLIB////////////////////////////////////////////////////////////
  static class CglibProxy {
    public static Object newProxy(Class<?> objectClass, MethodInterceptor methodInterceptor) {
      Enhancer enhancer = new Enhancer();
      enhancer.setSuperclass(objectClass);
      enhancer.setCallback(methodInterceptor);
      return enhancer.create();
    }
  }


  //////////////////////////////////////////////////////AOP Method Info///////////////////////////////////////////////////
  static class AopMethodInfo {

    final byte invokeType;
    final Method aopMethod;
    static byte INVOKE_TYPE_BEFORE = 0x01;
    static byte INVOKE_TYPE_AFTER = 0x02;
    static byte INVOKE_TYPE_AROUND = 0x03;
    static int BEFORE_INVOKE_POSITION = 0;
    static int AFTER_INVOKE_POSITION = 1;
    static int AROUND_INVOKE_POSITION = 2;
    static int AOP_INVOKE_ARRAY_SIZE = 3;

    static String SPLIT_CHAR = "#";

    static Object invoke(Map<Method, AopMethodInfo[]> methodMap, Object originObject, Object proxyObject, Method method, MethodProxy proxy, Object[] args) throws Throwable {
      AopMethodInfo before = getBefore(methodMap, method);
      AopMethodInfo after = getAfter(methodMap, method);
      AopMethodInfo around = getAround(methodMap, method);
      if (before != null) {
        Object[] objects = new Object[args.length + 2];
        objects[0] = originObject.getClass().getName();
        objects[1] = method.toString();
        System.arraycopy(args, 0, objects, 2, args.length);
        before.aopMethod.invoke(null, new Object[]{objects});
      }

      if (around != null) {
        Object[] objects = new Object[args.length + 3];
        objects[0] = true;
        objects[1] = originObject.getClass().getName();
        objects[2] = method.toString();
        System.arraycopy(args, 0, objects, 3, args.length);
        around.aopMethod.invoke(null, new Object[]{objects});
      }

      //real method
      Object result = method.invoke(originObject, args);

      if (after != null) {
        Object[] objects = new Object[args.length + 3];
        objects[0] = originObject.getClass().getName();
        objects[1] = method.getName();
        objects[2] = result;
        System.arraycopy(args, 0, objects, 3, args.length);
        after.aopMethod.invoke(null, new Object[]{objects});
      }

      if (around != null) {
        Object[] objects = new Object[args.length + 4];
        objects[0] = false;
        objects[1] = originObject.getClass().getName();
        objects[2] = method.toString();
        objects[3] = result;
        System.arraycopy(args, 0, objects, 4, args.length);
        around.aopMethod.invoke(null, new Object[]{objects});
      }
      return result;
    }

    static void registerAopObject(Map<Method, AopMethodInfo[]> methodMap, @Nullable Object object) {
      if (object == null) {
        return;
      }
      Aspect aspect = object.getClass().getAnnotation(Aspect.class);

      Method[] declaredMethods = object.getClass().getDeclaredMethods();

      String before = aspect.before();
      String after = aspect.after();
      String around = aspect.around();
      AopMethodInfo[] aopMethodArray = new AopMethodInfo[AOP_INVOKE_ARRAY_SIZE];
      if (!before.equals("")) {
        aopMethodArray[BEFORE_INVOKE_POSITION] = new AopMethodInfo(AopMethodInfo.INVOKE_TYPE_BEFORE, before);
      }

      if (!after.equals("")) {
        aopMethodArray[AFTER_INVOKE_POSITION] = new AopMethodInfo(AopMethodInfo.INVOKE_TYPE_AFTER, after);
      }

      if (!around.equals("")) {
        aopMethodArray[AROUND_INVOKE_POSITION] = new AopMethodInfo(AopMethodInfo.INVOKE_TYPE_AROUND, around);
      }

      for (Method declaredMethod : declaredMethods) {
        //register all method aop
        methodMap.put(declaredMethod, aopMethodArray);
      }
    }

    static void registerAspectMethod(Map<Method, AopMethodInfo[]> methodMap, Object object, Method method) {
      Aspect aspect = method.getAnnotation(Aspect.class);
      String after = aspect.after();
      String before = aspect.before();
      String around = aspect.around();

      //register one method
      AopMethodInfo[] aopMethodArray = methodMap.computeIfAbsent(method, (method1) -> new AopMethodInfo[AOP_INVOKE_ARRAY_SIZE]);

      if (!before.equals("")) {
        aopMethodArray[BEFORE_INVOKE_POSITION] = new AopMethodInfo(INVOKE_TYPE_BEFORE, before);
      }

      if (!after.equals("")) {
        aopMethodArray[AFTER_INVOKE_POSITION] = new AopMethodInfo(INVOKE_TYPE_AFTER, after);
      }

      if (!around.equals("")) {
        aopMethodArray[AROUND_INVOKE_POSITION] = new AopMethodInfo(INVOKE_TYPE_AROUND, around);
      }
    }

    @Nullable
    static AopMethodInfo getAround(Map<Method, AopMethodInfo[]> methodMap, @NonNull Method method) {
      AopMethodInfo[] aopMethodArray = methodMap.get(method);
      if (aopMethodArray == null) {
        return null;
      }

      return aopMethodArray[AROUND_INVOKE_POSITION];
    }

    @Nullable
    static AopMethodInfo getBefore(Map<Method, AopMethodInfo[]> methodMap, @NonNull Method method) {
      AopMethodInfo[] aopMethodArray = methodMap.get(method);
      if (aopMethodArray == null) {
        return null;
      }

      return aopMethodArray[BEFORE_INVOKE_POSITION];
    }

    @Nullable
    static AopMethodInfo getAfter(Map<Method, AopMethodInfo[]> methodMap, @NonNull Method method) {
      AopMethodInfo[] aopMethodInfoArray = methodMap.get(method);
      if (aopMethodInfoArray == null) {
        return null;
      }

      return aopMethodInfoArray[AFTER_INVOKE_POSITION];
    }


    AopMethodInfo(byte invokeType, String aopMethod) {
      this.invokeType = invokeType;
      this.aopMethod = parseAopString(aopMethod);
    }

    Method parseAopString(String aop) {
      String[] substring = aop.split(SPLIT_CHAR);
      if (substring.length != 2) {
        throw new IllegalArgumentException("error args for aop " + aop + ", you must define class#method ");
      }

      String className = substring[0];
      String methodName = substring[1];
      Class<?> clazz;
      Method method;
      try {
        clazz = Reflector.resolveClass(className);
        method = clazz.getDeclaredMethod(methodName, Object[].class);
        if (!Modifier.isStatic(method.getModifiers())) {
          throw new IllegalArgumentException("invoke aop method is not static " + method);
        }
      } catch (Exception e) {
        throw new RuntimeException(e);
      }

      return method;
    }
  }
}
